package soeasy.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import org.jfree.data.Range;
import org.jfree.data.xy.XYDataItem;
import org.jfree.data.xy.XYSeries;

public class DigitalizedXYSeries {

	private List<List<XYDataItem>> groupedItems;
	private List<Averager> groupedAveragers;
	private List<XYDataItem> items;
	private Range xValueRange;
	private double stepSize;

	public DigitalizedXYSeries(Range xValueRange, double stepSize) {
		this.xValueRange = xValueRange;
		this.stepSize = stepSize;

		this.items = new ArrayList<XYDataItem>();
		this.groupedItems = new ArrayList<List<XYDataItem>>();
		for (double index = xValueRange.getLowerBound(); index <= xValueRange
				.getUpperBound(); index += stepSize) {
			this.groupedItems.add(new ArrayList<XYDataItem>());
		}

		this.groupedAveragers = new ArrayList<Averager>();
		for (double index = xValueRange.getLowerBound(); index <= xValueRange
				.getUpperBound(); index += stepSize) {
			this.groupedAveragers.add(new Averager());
		}
	}

	public XYSeries toXYSeries(Comparable<?> key) {
		class AnonymousXYSeries extends XYSeries {

			private static final long serialVersionUID = 1L;

			public AnonymousXYSeries(Comparable<?> key) {
				super(key);
			}
			
			/**
			 * Sets a list of data in a single go
			 */
			@SuppressWarnings("unchecked")
			public void setData(List<XYDataItem> data) {
				this.data = data;
				if (this.getAutoSort()) {
					Collections.sort(this.data);
				}
			}
		}

		AnonymousXYSeries xySeries = new AnonymousXYSeries(key);
		xySeries.setData(getItems());

		return xySeries;
	}	

	/**
	 * Returns all items in one list.
	 * 
	 * @return
	 */
	public List<XYDataItem> getItems() {
		return this.items;
	}

	public int getItemCount() {
		return this.items.size();
	}

	public List<XYDataItem> getItemsAt(double xValue) {
		xValue = MathUtils.digitalize(xValue);
		if (xValueRange.contains(xValue)) {
			int index = (int) ((xValue - xValueRange.getLowerBound()) / stepSize);
			return this.groupedItems.get(index);
		} else {
			System.out.println("The xValue " + xValue
					+ " is out of the ranges.");
			throw new IndexOutOfBoundsException("The xValue " + xValue
					+ " is out of the ranges.");
		}
	}

	public List<XYDataItem> getItemsBetween(double fromXValue, double toXValue) {
		fromXValue = MathUtils.digitalize(fromXValue);
		toXValue = MathUtils.digitalize(toXValue);
		
		List<XYDataItem> items = new ArrayList<XYDataItem>();

		if (!xValueRange.contains(fromXValue)) {
			fromXValue = xValueRange.getLowerBound();
		}

		if (!xValueRange.contains(toXValue)) {
			toXValue = xValueRange.getUpperBound();
		}

		for (double xValue = fromXValue; xValue <= toXValue; xValue += stepSize) {
			int index = (int) ((xValue - xValueRange.getLowerBound()) / stepSize);
			items.addAll(this.groupedItems.get(index));
		}
		return items;
	}

	public Range getXValueRange() {
		return this.xValueRange;
	}

	public double getStepSize() {
		return this.stepSize;
	}

	public int size() {
		return this.items.size();
	}

	public void add(double xValue, double yValue) {
		xValue = MathUtils.digitalize(xValue);
		XYDataItem newItem = new XYDataItem(xValue, yValue);
		getItemsAt(xValue).add(newItem);
		getAveragerAt(xValue).update(yValue);
		this.items.add(newItem);
	}
	
	public int indexOf(double xValue) {
		return (int) ((xValue - xValueRange.getLowerBound()) / stepSize);
	}

	public Averager getAveragerAt(double xValue) {
		xValue = MathUtils.digitalize(xValue);
		int index = (int) ((xValue - xValueRange.getLowerBound()) / stepSize);
		return this.groupedAveragers.get(index);
	}

	public List<XYDataItem> getSortedItemsAt(double xValue) {
		List<XYDataItem> items = getItemsAt(xValue);
		Collections.sort(items, new Comparator<XYDataItem>() {
			@Override
			public int compare(XYDataItem o1, XYDataItem o2) {
				double diff = o1.getYValue() - o2.getYValue();
				if (diff == 0.0) {
					return 0;
				} else if (diff > 0.0) {
					return 1;
				} else {
					return -1;
				}
			}
		});
		return items;
	}

	public double getAverageYValueBetween(double fromXValue, double toXValue) {				
		if (!xValueRange.contains(fromXValue)) {
			fromXValue = xValueRange.getLowerBound();
		}

		if (!xValueRange.contains(toXValue)) {
			toXValue = xValueRange.getUpperBound();
		}

		Averager averager = new Averager();
		for (double xValue = fromXValue; xValue <= toXValue; xValue += stepSize) {
			Averager averagerAt = getAveragerAt(xValue);
			averager.update(averagerAt);
		}

		if (averager.getElementCount() == 0) {			
			return Double.NaN;
		} else {
			return averager.getAverage();
		}
	}
	
	public double getWegihtedAverageYValueBetween(double fromXValue, double toXValue) {
		double midXValue = (toXValue + fromXValue) / 2;
		int maxWeight = (int)((toXValue - midXValue) / 0.5) + 1;
		
		if (!xValueRange.contains(fromXValue)) {
			fromXValue = xValueRange.getLowerBound();
		}

		if (!xValueRange.contains(toXValue)) {
			toXValue = xValueRange.getUpperBound();
		}

		double total = 0.0;
		int totalWeight = 0;
		for (double xValue = fromXValue; xValue <= toXValue; xValue += stepSize) {
			int weight = maxWeight - Math.abs((int)((midXValue - xValue)/0.5));
			Averager averagerAt = getAveragerAt(xValue);
			total += averagerAt.getAverage() * weight;
			totalWeight += weight;
		}

		return total / totalWeight;
	}

	public XYDataItem getDataItem(int index) {
		return this.items.get(index);
	}	
}
